<?php

declare(strict_types=1);

namespace Tpetry\PostgresqlEnhanced\Support\Helpers;

use Closure;
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
use Illuminate\Database\Query\Expression as ExpressionClass;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Fluent;
use Illuminate\Support\Str;
use Tpetry\PostgresqlEnhanced\Schema\Grammars\Grammar;

/**
 * @internal
 */
class MigrationIndex
{
    /**
     * @param 'index'|'primary'|'unique' $type
     */
    public function compileCommand(Grammar $grammar, string $table, Fluent $command, string $type): string
    {
        // If the index is partial index using a closure a dummy query builder is provided to the closure. The query is
        // then transformed to a static query and the select part is removed to only keep the condition.
        if ($command['where'] instanceof Closure) {
            $query = ($command['where'])(DB::query());
            $command['where'] = trim(str_replace('select * where', '', Query::toSql($query)));
        }

        // If the storage parameters for the index are provided in array form they need to be serialized to PostgreSQL's
        // string format.
        if (\is_array($command['with'])) {
            $with = array_map(fn (mixed $value) => match ($value) {
                true => 'on',
                false => 'off',
                default => (string) $value,
            }, $command['with']);
            $with = array_map(fn (string $value, string $key) => "{$key} = {$value}", $with, array_keys($with));
            $command['with'] = implode(', ', $with);
        }

        // If the additions for column specifications are used the columns need to be columnized different to the
        // standard laravel logic which is expecting plain references.
        // Note: The ExpressionContract has been added with Laravel 10, older versions used the ExpressionClass.
        $columns = array_map(function (string|ExpressionContract|ExpressionClass $column) use ($grammar): string {
            // Expressions generated by rawIndex() should by definition not processed anymore and used as provided
            if ($column instanceof ExpressionClass || $column instanceof ExpressionContract) {
                return $grammar->getValue($column);
            }

            // When a functional index or escaped column name  is provided the column string is already a valid raw
            // column index string and can be used exactly as provided.
            if (Str::startsWith($column, ['(', '"'])) {
                return $column;
            }

            // In case index parameters are provided the column needs to escaped correctly and the rest is provided
            // exactly as provided.
            return $grammar->columnizeWithSuffix([$column]);
        }, $command['columns']);

        // A fulltext index needs special handling to wrap the columns into to_tsvector calls.
        if ('fulltext' === $command['name']) {
            $columns = array_map(function (string $column, int $index) use ($command, $grammar): string {
                $language = $command['language'] ?? 'english';

                return match (isset($command['weight'][$index])) {
                    true => "setweight(to_tsvector({$grammar->quoteString($language)}, {$column}), {$grammar->quoteString($command['weight'][$index])})",
                    false => "to_tsvector({$grammar->quoteString($language)}, {$column})",
                };
            }, $columns, array_keys(array_values($columns)));
            $columns = ['('.implode(' || ', $columns).')'];
        }

        $index = match ($type) {
            'index', 'unique' => [
                'unique' === $type ? 'create unique index' : 'create index',
                $command['concurrently'] ? 'concurrently' : null,
                $command['ifNotExists'] ? 'if not exists' : null,
                $grammar->wrap($command['index']),
                'on',
                $grammar->wrapTable($table),
                $command['algorithm'] ? "using {$command['algorithm']}" : null,
                '('.implode(', ', $columns).')',
                $command['include'] ? 'include ('.implode(',', $grammar->wrapArray(Arr::wrap($command['include']))).')' : null,
                $command['nullsNotDistinct'] ? 'nulls not distinct' : null,
                $command['with'] ? "with ({$command['with']})" : null,
                $command['where'] ? "where {$command['where']}" : null,
            ],
            'primary' => [
                'alter table',
                $grammar->wrapTable($table),
                'add primary key ('.implode(', ', $columns).')',
            ],
        };
        $sql = implode(' ', array_filter($index, fn (?string $part) => filled($part)));

        return $sql;
    }

    public function createCommand(string $type, string $name, array|string $columns, ?string $algorithm): Fluent
    {
        $columns = (array) $columns;

        return new Fluent(compact('type', 'columns', 'algorithm') + ['index' => $name]);
    }

    public function createName(string $type, string $prefix, string $table, array $columns): string
    {
        $table = match (str_contains($table, '.')) {
            true => substr_replace("{$table}.{$prefix}", strrpos($table, '.'), 1),
            false => $prefix.$table,
        };
        $columns = array_map(fn (string $column) => Str::before($column, ' '), $columns);

        $index = strtolower($table.'_'.implode('_', $columns).'_'.$type);

        return str_replace(['-', '.'], '_', $index);
    }
}
